package pl.com.qapps.unity;

import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;

import pl.com.qapps.unity.LauncherPb.ItemEvent;
import pl.com.qapps.unity.LauncherPb.ItemEvent.Property;
import pl.com.qapps.unity.LauncherPb.ItemEvent.Signal;
import pl.com.qapps.unity.LauncherPb.ItemEvent.Type;
import pl.com.qapps.unity.LauncherPb.ItemEvent.Value;
import pl.com.qapps.unity.LauncherPb.ItemPb;
import pl.com.qapps.unity.LauncherPb.QuicklistEvent;
import pl.com.qapps.unity.LauncherPb.QuicklistPb;
import pl.com.qapps.unity.Quicklist.Item;
import pl.com.qapps.unity.Unity.Bus;
import pl.com.qapps.unity.Unity.InitHandler;
import pl.com.qapps.unity.UnityPb.Request;
import pl.com.qapps.unity.UnityPb.Response;

/**
 * Represents a Unity quicklist. After creating one set it to a
 * {@link UnityLauncher} by invoking
 * {@linkplain UnityLauncher#setQuicklist(Quicklist_LinkedList)} to make it
 * appear on the Unity launcher. <br>
 * <br>
 * <br>
 * If you try to add the same item twice or to two different quicklists it won't
 * take any effect. <br>
 * <br>
 * <br
 * . The items can be added/removed 'on the fly', not only in construct time.
 * Even after quicklist is set to launcher the changes will be applied. <br>
 * <br>
 * 
 * This list itself is not ThreadSafe. Multiple threads should not use it
 * without synchronization. You can use the Collections.synchronizedList(List)
 * if you like are synchronize on your own. Sample usage code:
 * 
 * <pre>
 * <code>
 * Quicklist list = new Quicklist();
 * 
 * Item i1 = ...
 * Item i2 = ...
 * Item i3 = ...
 * 
 * list.add(i1);
 * list.add(Quicklist.SEPARATOR);
 * list.add(i2);
 * list.add(i3);
 * 
 * Item[] items = ...
 * Quicklist list2 = new Quicklist(items);
 * 
 * UnityLauncher launcher = ...
 * launcher.setQuicklist(list);
 * // the quicklist set above gets replaced
 * launcher.setQuicklist(list2);
 * //clear existing one
 * list2.clear();
 * // add items to existing one
 * list2.addAll(list);
 * // or add empty one
 * launcher.setQuicklist(new Quicklist());
 * //or set none
 * launcher.setQuicklist(null); 
 * </code>
 * </pre>
 * 
 * @author Jakub Dykowski
 * 
 */
public final class Quicklist extends AbstractList<Item> implements List<Item> {

	private static final Map<Long, Item> itemStack = new WeakHashMap<>();
	private static Logger log = Logger.getLogger("Quicklist");

	static {
		// logging
		try {
			FileHandler fh = new FileHandler("quicklist.log", true);
			fh.setFormatter(new SimpleFormatter());
			log.addHandler(fh);
			log.setLevel(Level.OFF);
		} catch (SecurityException | IOException e) {
			e.printStackTrace(System.err);
		}
		// protobuf extension
		Unity.register(LauncherPb.quicklistResponse);
		Unity.register(LauncherPb.itemResponse);
		Unity.register(LauncherPb.itemSignal);
		// item callback from unity backend
		Unity.register(new Unity.SignalHandler() {

			@Override
			public void onSignal(Response res) {
				if (res.hasExtension(LauncherPb.itemSignal)) {
					Signal signal = res.getExtension(LauncherPb.itemSignal);
					Item item = itemStack.get(signal.getItem().getId());
					if (item != null)
						try {
							item.applySignal(signal.getItem());
						} catch (Exception e) {
							log.log(Level.INFO,
									"error while applying event to item, suppressed",
									e);
						}
				}
			}

		});
	}

	/**
	 * It's a base class for all types of quicklist items. <br>
	 * <br>
	 * To make an item act as real quicklist item, first add it to
	 * {@linkplain Quicklist_LinkedList} and then add the quicklist to
	 * {@linkplain Launcher}. <br>
	 * <br>
	 * You can change the item attributes before the quicklist gets added to
	 * launcher or even after that is done, in both cases the changes will be
	 * applied sooner or later (immediately, or after
	 * {@linkplain Quicklist_LinkedList} will be added to {@linkplain Launcher})
	 * so that you can change the attributes on the fly, not just once at the
	 * beginning.
	 * 
	 * It represents single quicklist entry.
	 * 
	 * NotThreadSafe.
	 * 
	 * @author Jakub Dykowski
	 * 
	 */
	public static abstract class Item {

		private static final AtomicLong count = new AtomicLong(0);
		private final long id = count.incrementAndGet();

		/**
		 * Cached label property
		 */
		private String label;
		/**
		 * Cached visible property
		 */
		private boolean visible;
		/**
		 * Cached enabled property
		 */
		private boolean enabled;

		private Item() throws LauncherException {
			this(null);
		}

		private Item(String label) throws LauncherException {
			this(label, true, true);
		}

		private Item(String label, boolean enabled, boolean visible)
				throws LauncherException {
			this.label = label;
			this.visible = visible;
			this.enabled = enabled;
			itemStack.put(id, this);
		}

		public final synchronized Item setLabel(final String label)
				throws LauncherException {
			set(this, Property.LABEL, Value.newBuilder().setString(label)
					.build());
			this.label = label;
			return this;
		}

		public final String getLabel() throws LauncherException {
			return label;
		}

		/**
		 * If item is disabled it can't get clicked.
		 * 
		 * @param enabled
		 *            <code>true</code> if enable, <code>false</code> otherwise
		 * @return
		 * @throws LauncherException
		 *             if any internal error occurs
		 */
		public final synchronized Item setEnabled(final boolean enabled)
				throws LauncherException {
			set(this, Property.ENABLED, Value.newBuilder().setBool(enabled)
					.build());
			this.enabled = enabled;
			return this;
		}

		/**
		 * Checks whether item is enabled or not.
		 * 
		 * @return <code>true</code> if enabled, <code>false</code> otherwise
		 */
		public final boolean isEnabled() throws LauncherException {
			return enabled;
		}

		/**
		 * Make item visible or not.<br>
		 * <br>
		 * Instead of adding or removing items 'on the fly' which is impossible
		 * make them visible or not.
		 * 
		 * @param visible
		 *            <code>true</code> if visible, <code>false</code> otherwise
		 * @return
		 * @throws LauncherException
		 *             if any internal error occurs
		 */
		public final synchronized Item setVisible(final boolean visible)
				throws LauncherException {
			set(this, Property.VISIBLE, Value.newBuilder().setBool(visible)
					.build());
			this.visible = visible;
			return this;
		}

		/**
		 * Checks whether item is visible or not.
		 * 
		 * @return <code>true</code> if visible, <code>false</code> otherwise
		 */
		public final boolean isVisible() throws LauncherException {
			return visible;
		}

		@Override
		public boolean equals(Object obj) {
			if (!(obj instanceof Item))
				return false;
			Item other = (Item) obj;
			return id == other.id;
		}

		@Override
		public int hashCode() {
			return (int) (id ^ (id >>> 32));
		}

		@Override
		public final String toString() {
			return "<Item '" + label + "'>";
		}

		/**
		 * Should be only invoked by {@link EventDispatcher} from its dispatch
		 * thread. This method should update the item state, trigger this item
		 * events and also apply its super class.
		 * 
		 * @param proto
		 *            the state to be applied
		 */
		void applySignal(ItemPb proto) {
			if (proto != null) {
				if (proto.hasVisible()) {
					this.visible = proto.getVisible();
				}
				if (proto.hasEnabled()) {
					this.enabled = proto.getEnabled();
				}
				if (proto.hasLabel()) {
					this.label = proto.getLabel();
				}
			}
		}

		@Override
		protected final void finalize() throws Throwable {
			delete(this);
		}

		void buildProto(ItemPb.Builder builder) {
			builder.setId(id);
			builder.setVisible(visible);
			builder.setEnabled(enabled);
			if (label != null)
				builder.setLabel(label);
		}

		final ItemPb toProto() {
			ItemPb.Builder builder = ItemPb.newBuilder();
			buildProto(builder);
			return builder.build();
		}
	}

	/**
	 * Represents an item with possibly associated action to it, or none.
	 * 
	 * ThreadSafe.
	 * 
	 * @author Jakub Dykowski
	 * 
	 */
	public final static class Action extends Item {

		private final Runnable action;

		/**
		 * If label is <code>null</code> then "" will be displayed. The action
		 * is being executed each time user click on the item. <br>
		 * <br>
		 * Note that it is possible to pass a <code>null</code> action as
		 * argument but then the item won't do anything after gets clicked.
		 * 
		 * @param label
		 *            can be <code>null</code>
		 * @param action
		 *            can be <code>null</code>
		 */
		public Action(String label, Runnable action) throws LauncherException {
			super(label);
			this.action = action;
			Unity.register(new CreateItemOnInit(this));
		}

		@Override
		void applySignal(ItemPb proto) {
			super.applySignal(proto);
			if (action != null)
				action.run();
		}

		@Override
		void buildProto(ItemPb.Builder builder) {
			super.buildProto(builder);
			builder.setType(ItemPb.Type.ACTION);
		}
	}

	/**
	 * It is an horizontal which separates items from themselves. Label
	 * attribute does not change anything neither the enable/disable attribute.
	 * 
	 * ThreadSafe.
	 * 
	 * @author Jakub Dykowski
	 * 
	 */
	static final class Separator extends Item {

		/**
		 * A horizontal divider, separating items from each other allowing for
		 * making group of items. Has got no action and {@link #getAction()}
		 * return always <code>null</code>.
		 */
		public Separator() {
			super(null);
			Unity.register(new CreateItemOnInit(this));
		}

		@Override
		void buildProto(ItemPb.Builder builder) {
			super.buildProto(builder);
			builder.setType(ItemPb.Type.SEPARATOR);
		}
	}

	/**
	 * It is a checkbox, which can be in two states, selected or not. To use
	 * this class inherit from it overriding its abstract method
	 * {@link #onSelected(boolean)}, good practice is to make anonymous classes:
	 * 
	 * <pre>
	 * <code>
	 * 	Select checkbox = new Select("Item1", true) {
	 * 		
	 * 		public void selected(boolean selected) {
	 * 			// here write your code which is activated 
	 * 			// each time the item changes its state
	 * 		}
	 * 
	 * 	}
	 * </code>
	 * </pre>
	 * 
	 * ThreadSafe.
	 * 
	 * @author Jakub Dykowski
	 * 
	 */
	public static abstract class Select extends Item {

		private boolean selected;

		/**
		 * A shorthand for {@link #Select(label, selected)}
		 * 
		 * @param label
		 *            to be displayed, can be <code>null</code>
		 */
		public Select(String label) {
			this(label, false);
		}

		/**
		 * Represents a simple checkbox, which can be selected or not.
		 * 
		 * @param label
		 *            to be displayed, can be <code>null</code>
		 * @param selected
		 *            whether item should be selected or not
		 */
		public Select(String label, final boolean selected)
				throws LauncherException {
			super(label);
			this.selected = selected;
			Unity.register(new CreateItemOnInit(this));
		}

		/**
		 * Manually set whether this item should be selected or not. If item has
		 * not been added yet it only causes the attribute to change.
		 * 
		 * @param selected
		 * @return
		 */
		public final synchronized Select setSelected(final boolean selected) {
			if (selected != isSelected())
				set(this, Property.SELECTED,
						Value.newBuilder().setBool(selected).build());
			this.selected = selected;
			return this;
		}

		/**
		 * Checks whether item is selected or not.
		 * 
		 * @return <code>true</code> if selected, <code>false</code> otherwise
		 */
		public final boolean isSelected() {
			// cached
			return selected;
			// directly
			// return get(this, Property.SELECTED).getBool();
		}

		@Override
		final synchronized void applySignal(ItemPb proto) {
			super.applySignal(proto);
			if (proto.hasSelected()) {
				fireSelected(proto.getSelected());
			}
		}

		@Override
		final void buildProto(ItemPb.Builder builder) {
			super.buildProto(builder);
			builder.setSelected(selected);
			builder.setType(ItemPb.Type.SELECT);
		}

		/**
		 * This method is invoked each time item changes its state (selected or
		 * not).
		 * 
		 * @param selected
		 *            <code>true</code> if selected, <code>false</code>
		 *            otherwise
		 */
		protected abstract void onSelected(final boolean selected);

		public synchronized void fireSelected(final boolean selected) {
			if (selected != isSelected()) {
				setSelected(selected);
				Unity.dispatch(new Runnable() {

					@Override
					public void run() {
						synchronized (Select.this) {
							if (Select.this.isSelected() == selected)
								try {
									onSelected(selected);
								} catch (Throwable thrown) {
									log.log(Level.INFO,
											"error while dispatching Select item event, suppressed",
											thrown);
								}
							else {
								log.log(Level.INFO,
										"Select item event is out of date, skipping",
										Select.this);
							}
						}
					}

				});
			}
		}
	}

	/**
	 * Allows to make a group of items where just one and always one is
	 * selected, if group consists of only one item then it will be always be
	 * selected. <br>
	 * <br>
	 * In the same radio buttons group there is always one and only one selected
	 * radio button.
	 * 
	 * ThreadSafe.
	 * 
	 * @author Jakub Dykowski
	 * 
	 */
	public static final class Radio<E> extends Item {

		private final RadioGroup<E> group;
		private final E option;

		/**
		 * A shorthand for Radio(label, group, <code>true</code>) Always the
		 * last item added to quicklist (in the same group) which is marked as
		 * 'selected' will be selected. if radio button is added and no item in
		 * the same group has been selected or this is the first one then it
		 * will be selected automatically.
		 * 
		 * @param label
		 * @param group
		 */
		public Radio(RadioGroup<E> group, E option) {
			this(null, group, option);
		}

		/**
		 * Always the last item added to quicklist (in the same group) which is
		 * marked as 'selected' will be selected. if radio button is added and
		 * no item in the same group has been selected or this is the first one
		 * then it will be selected automatically.
		 * 
		 * @param label
		 * @param group
		 * @param option
		 * @throws NullPointerException
		 */
		public Radio(String label, final RadioGroup<E> group, E option)
				throws NullPointerException {
			super(label);
			if (group == null)
				throw new NullPointerException("group cannot be null");
			this.group = group;
			if (option == null)
				throw new NullPointerException("option cannot be null");
			this.option = option;

			Unity.register(new CreateItemOnInit(this));
			if (group.getSelected() == null)
				Unity.dispatchSynchronously(new Runnable() {

					@Override
					public void run() {
						if (group.getSelected() == null)
							select();
					}

				});
		}

		public RadioGroup<E> getGroup() {
			return group;
		}

		/**
		 * Checks whether item is selected or not.
		 * 
		 * @return <code>true</code> if selected, <code>false</code> otherwise
		 */
		public boolean isSelected() {
			return group.getSelected() == this;
		}

		/**
		 * Programmaticcally marks this radio button as selected (where the
		 * previous selection is unmarked). This method does not invoke
		 * selection handlers.
		 * 
		 * @return the item itself
		 */
		public synchronized Radio<E> select() {
			if (group.selected != this) {
				if (group.selected != null)
					group.selected.markSelected(false);
				this.markSelected(true);
				group.selected = this;
			}
			return this;
		}

		public synchronized Radio<E> fireSelected() {
			if (group.getSelected() != this) {
				select();
				Unity.dispatch(new Runnable() {

					@Override
					public void run() {
						synchronized (Radio.this) {
							if (group.selected == Radio.this)
								try {
									group.onSelected(Radio.this.option);
								} catch (Throwable thrown) {
									log.log(Level.INFO,
											"error while dispatching Radio item event, suppressed",
											thrown);
								}
							else
								log.log(Level.INFO,
										"Radio button event is out of date, skipped...",
										Radio.this);
						}
					}
				});
			}
			return this;
		}

		/**
		 * Returns the option that this item represents.
		 * 
		 * @return non <code>null</code>
		 */
		public final E getOption() {
			return option;
		}

		@Override
		final synchronized void applySignal(ItemPb proto) {
			super.applySignal(proto);
			if (proto.getSelected())
				fireSelected();
		}

		@Override
		void buildProto(ItemPb.Builder builder) {
			super.buildProto(builder);
			builder.setSelected(getGroup().getSelected() == this);
			builder.setType(ItemPb.Type.RADIO);
		}

		private void markSelected(boolean selected) {
			set(this, Property.SELECTED, Value.newBuilder().setBool(selected)
					.build());
		}
	}

	/**
	 * An object that receives callback when radio button selection changed.
	 * 
	 * ThreadSafe.
	 * 
	 * @author Jakub Dykowski
	 * 
	 * @param <E>
	 */
	public static abstract class RadioGroup<E> {

		private Radio<E> selected = null;

		protected abstract void onSelected(E option);

		/**
		 * Currently selected item, or <code>null</code> if non has been set yet
		 * (which is done when first {@code Radio}) is created.
		 * 
		 * @return bitch
		 */
		public final Radio<E> getSelected() {
			return selected;
		}
	}

	private static final AtomicLong count = new AtomicLong(0L);
	private final long id = count.incrementAndGet();
	private final List<Item> items = new ArrayList<>();

	// private final int from;

	/**
	 * Creates empty quicklist with no items.
	 */
	public Quicklist() {
		Unity.register(new CreateQuicklistOnInit(this));
	}

	/**
	 * Creates quicklist initially filled by items from array.
	 * 
	 * @param items
	 *            non <code>null</code> arrays of items
	 */
	public Quicklist(Item... items) throws NullPointerException {
		Objects.requireNonNull(items, "items cannot be null");
		for (Item item : items) {
			if (item == null)
				throw new NullPointerException("array cannot contain null");
			this.items.add(item);
		}
		Unity.register(new CreateQuicklistOnInit(this));
	}

	/**
	 * Creates quicklist initially filled by given items.
	 * 
	 * @param items
	 *            non <code>null</code> {@linkplain Iterable} of items
	 */
	public Quicklist(Iterable<Item> items) {
		if (items == null)
			throw new NullPointerException("items cannot be null");
		for (Item item : items) {
			if (item != null)
				this.items.add(item);
		}
		Unity.register(new CreateQuicklistOnInit(this));
	}

	/**
	 * @throws NullPointerException
	 *             if element is <code>null</code>
	 */
	@Override
	public boolean add(Item element) throws NullPointerException {
		// if(from == DEFAULT_FROM_INDEX) {
		// if (element == null)
		// throw new NullPointerException("item cannot be null");
		// addInternal(size(), element);
		// } else
		addInternal(element);
		return items.add(element);
	}

	/**
	 * @throws NullPointerException
	 *             id element is <code>null</code>
	 */
	@Override
	public void add(int index, Item element) throws NullPointerException {
		addInternal(/* from + */index, element);
		items.add(index, element);
	}

	/**
	 * @throws NullPointerException
	 *             if argument is {@link NullPointerException}
	 * @throws ClassCastException
	 *             if argument is not instance of {@link Item}
	 */
	@Override
	public boolean remove(Object o) throws NullPointerException,
			ClassCastException {
		removeInternal((Item) o);
		return items.remove(o);
	}

	@Override
	public Item remove(int index) {
		// TODO test
		Item toRemove = items.get(index);
		remove(toRemove);
		return toRemove;
	}

	@Override
	public Item get(int index) {
		return items.get(index);
	}

	@Override
	public Item set(int index, Item item) {
		if (item == null)
			throw new NullPointerException("null elements are not supported");
		if (index < 0 || index > size())
			throw new IndexOutOfBoundsException(
					"index has to be >= 0 and <= size()");
		Item result = remove(index);
		add(index, item);
		return result;

	}

	@Override
	public int size() {
		return items.size();
	}

	// @Override
	// public List<Item> subList(int fromIndex, int toIndex) throws
	// IndexOutOfBoundsException {
	// return new Quicklist(id, items.subList(fromIndex, toIndex), from +
	// fromIndex);
	// }

	@Override
	public void clear() {
		// TODO make better exception handling (figure out that item were
		// deleted or
		// not)
		QuicklistEvent.Builder evt = QuicklistEvent.newBuilder()
				.setQuicklist(QuicklistPb.newBuilder().setId(id).build())
				.setType(QuicklistEvent.Type.REMOVE);
		for (Item item : items)
			evt.addItem(ItemPb.newBuilder().setId(item.id).build());
		send(evt.build(), null);
		items.clear();
	}

	@Override
	public boolean addAll(Collection<? extends Item> c)
			throws NullPointerException {
		if (c == null)
			throw new NullPointerException("Collection cannot be null");
		QuicklistEvent.Builder evt = QuicklistEvent.newBuilder()
				.setQuicklist(QuicklistPb.newBuilder().setId(id).build())
				.setType(QuicklistEvent.Type.ADD);
		for (Item item : c) {
			if (item == null)
				throw new NullPointerException();
			evt.addItem(ItemPb.newBuilder().setId(item.id).build());
		}
		send(evt.build(), null);
		return items.addAll(c);
	}
// TODO figure out why this is were commented (even removed), batch add
//	@Override
//	public boolean addAll(int index, Collection<? extends Item> c)
//			throws NullPointerException, IndexOutOfBoundsException {
//		if (index < 0 || index > size())
//			throw new IndexOutOfBoundsException();
//		// return subList(0,index).addAll(c);
//		QuicklistEvent.Builder evt = QuicklistEvent.newBuilder()
//				.setQuicklist(QuicklistPb.newBuilder().setId(id).build())
//				.setType(QuicklistEvent.Type.ADD).setIndex(index);
//		for (Item item : c) {
//			if (item == null)
//				throw new NullPointerException();
//			evt.addItem(ItemPb.newBuilder().setId(item.id).build());
//		}
//		send(evt.build(), null);
//		return items.addAll(index, c);
//	}

	@Override
	public boolean contains(Object o) {
		if (o == null)
			throw new NullPointerException("null is not supported");
		return items.contains(o);
	}

	@Override
	public boolean containsAll(Collection<?> c) throws NullPointerException {
		if (c == null)
			throw new NullPointerException("Collection cannot be null");
		return items.contains(c);
	}

	@Override
	public boolean equals(Object obj) {
		return items.equals(obj);
	}

	@Override
	public int hashCode() {
		return (int) (id ^ (id >>> 32));
	}

	@Override
	public int indexOf(Object o) throws NullPointerException {
		if (o == null)
			throw new NullPointerException("null is not supported");
		return items.indexOf(o);
	}

	@Override
	public boolean isEmpty() {
		return items.isEmpty();
	}

	@Override
	public int lastIndexOf(Object o) throws NullPointerException {
		if (o == null)
			throw new NullPointerException("null is not supported");
		return items.lastIndexOf(o);
	}

	// @Override
	// public ListIterator<Item> listIterator() {
	// return listIterator(0);
	// }

	// @Override
	// public ListIterator<Item> listIterator(final int index) {
	// return new ListIterator<Item>(){
	//
	// int pos = index;
	// Item current = null;
	// ListIterator<Item> wrapped = items.listIterator(index);
	//
	// @Override
	// public void add(Item item) {
	// addInternal(pos++, item);
	// wrapped.add(item);
	// }
	//
	// @Override
	// public boolean hasNext() {
	// return wrapped.hasNext();
	// }
	//
	// @Override
	// public boolean hasPrevious() {
	// return wrapped.hasPrevious();
	// }
	//
	// @Override
	// public Item next() {
	// ++pos;
	// return current = wrapped.next();
	// }
	//
	// @Override
	// public int nextIndex() {
	// return wrapped.nextIndex();
	// }
	//
	// @Override
	// public Item previous() {
	// ++pos;
	// return wrapped.previous();
	// }
	//
	// @Override
	// public int previousIndex() {
	// return wrapped.previousIndex();
	// }
	//
	// @Override
	// public void remove() {
	// if(current == null)
	// throw new
	// IllegalStateException("neither next() not previous() has been called yet");
	// // TODO replace with removeInternal(int) which needs to be implemented
	// removeInternal(current);
	// --pos;
	// wrapped.remove();
	// }
	//
	// @Override
	// public void set(Item arg0) {
	// // implement ListIterator.set(Item) but setInternal(int,Item) needs to be
	// implemented either
	// throw new UnsupportedOperationException(
	// "not supported yet");
	// }
	//
	// };
	// }

	// @Override
	// public boolean removeAll(Collection<?> c) throws NullPointerException {
	// if(c == null)
	// throw new NullPointerException("Collection cannot be null");
	// boolean changed = false;
	// for(Object toRemove : c) {
	// changed |= remove(toRemove);
	// }
	// return changed;
	// }

	// @Override
	// public boolean retainAll(Collection<?> c) {
	// if(c == null)
	// throw new NullPointerException("Collection cannot be null");
	// Iterator<Item> iter = items.iterator();
	// boolean changed = false;
	// loop:
	// while(iter.hasNext()) {
	// Item current = iter.next();
	// for(Object other : c) {
	// if(other.equals(current))
	// continue loop;
	// }
	// removeInternal(current);
	// iter.remove();
	// changed = true;
	// }
	// return changed;
	// }

	@Override
	public Object[] toArray() {
		return items.toArray();
	}

	@Override
	public <T extends Object> T[] toArray(T[] a) {
		return items.toArray(a);
	}

	@Override
	public Iterator<Item> iterator() {
		return new Iterator<Item>() {

			private final Iterator<Item> wrapped = items.iterator();
			private Item current = null;

			@Override
			public boolean hasNext() {
				return wrapped.hasNext();
			}

			@Override
			public Item next() {
				return current = wrapped.next();
			}

			@Override
			public void remove() {
				if (current == null)
					throw new IllegalStateException();
				removeInternal(current);
				wrapped.remove();
			}

		};
	}

	@Override
	public String toString() {
		return items.toString();
	}

	protected void finalize() throws Throwable {
		delete(this);
	};

	QuicklistPb toProto() {
		QuicklistPb.Builder builder = QuicklistPb.newBuilder().setId(this.id);
		for (Item item : this) {
			builder.addItem(item.toProto());
		}
		return builder.build();
	}

	private void addInternal(Item item) throws NullPointerException,
			IndexOutOfBoundsException {
		if (item == null)
			throw new NullPointerException("null elements are not supported");
		QuicklistEvent evt = QuicklistEvent.newBuilder()
				.setQuicklist(QuicklistPb.newBuilder().setId(id).build())
				.addItem(ItemPb.newBuilder().setId(item.id).build())
				.setType(QuicklistEvent.Type.ADD).build();
		send(evt, null);
	}

	private void addInternal(int index, Item item) throws NullPointerException,
			IndexOutOfBoundsException {
		if (item == null)
			throw new NullPointerException("null elements are not supported");
		if (index < 0 || index > size())
			throw new IndexOutOfBoundsException(
					"index has to be >= 0 and <= size()");
		QuicklistEvent evt = QuicklistEvent.newBuilder()
				.setQuicklist(QuicklistPb.newBuilder().setId(id).build())
				.addItem(ItemPb.newBuilder().setId(item.id).build())
				.setIndex(/* from + */index).setType(QuicklistEvent.Type.ADD)
				.build();
		send(evt, null);
	}

	private void removeInternal(Item item) {
		if (item == null)
			throw new NullPointerException("null is not supported");
		ItemPb itemProto = ItemPb.newBuilder().setId(item.id).build();
		QuicklistEvent evt = QuicklistEvent.newBuilder()
				.setType(QuicklistEvent.Type.REMOVE)
				.setQuicklist(QuicklistPb.newBuilder().setId(this.id).build())
				.addItem(itemProto).build();
		send(evt, null);
	}

	private static void delete(Quicklist quicklist) throws LauncherException {
		QuicklistPb proto = QuicklistPb.newBuilder().setId(quicklist.id)
				.build();
		QuicklistEvent evt = QuicklistEvent.newBuilder()
				.setType(QuicklistEvent.Type.DELETE).setQuicklist(proto)
				.build();
		send(evt, null);
	}

	// TODO przeciążyć create
	private static void create(Quicklist quicklist, Bus bus)
			throws LauncherException {
		QuicklistEvent evt = QuicklistEvent.newBuilder()
				.setType(QuicklistEvent.Type.CREATE)
				.setQuicklist(quicklist.toProto()).build();
		send(evt, bus);
	}

	private static void set(Item item, Property property, Value value) {
		ItemPb proto = ItemPb.newBuilder().setId(item.id).build();
		Response res = send(ItemEvent.newBuilder().setType(ItemEvent.Type.SET)
				.setItem(proto).setName(property).setValue(value).build());

		if (res.hasExtension(LauncherPb.itemResponse)) {
			ItemEvent.Value resValue = res
					.getExtension(LauncherPb.itemResponse);
			if (value.hasBool() && value.getBool() != resValue.getBool())
				throw new AssertionError(property + " returned: " + resValue
						+ " and supposed to: " + value);
		}
	}

	/*
	 * If properties should not be cached use this to get them, instead from
	 * cache (member fields)
	 */
	@SuppressWarnings("unused")
	private static Value get(Item item, Property property) {
		ItemPb proto = ItemPb.newBuilder().setId(item.id).build();
		Response res = send(ItemEvent.newBuilder().setType(ItemEvent.Type.GET)
				.setItem(proto).setName(property).build());

		if (!res.hasExtension(LauncherPb.itemResponse)) {
			System.out.println("the response itself:\n" + res);
			throw new LauncherException("no response returned", true);
		}
		return res.getExtension(LauncherPb.itemResponse);
	}

	private static void create(Item item, Bus bus) {
		send(ItemEvent.newBuilder().setType(Type.CREATE)
				.setItem(item.toProto()).build(), bus);
	}

	private static void delete(Item item) {
		ItemPb proto = ItemPb.newBuilder().setId(item.id).build();
		send(ItemEvent.newBuilder().setType(Type.DELETE).setItem(proto).build());
	}

	private static Response send(ItemEvent evt) {
		return send(evt, null);
	}

	private static Response send(ItemEvent evt, Bus bus) {
		try {
			if (bus == null)
				bus = Unity.connect();
			return bus.send(Request.newBuilder()
					.setExtension(LauncherPb.itemEvent, evt).build());

		} catch (IllegalStateException | IOException e) {
			throw new LauncherException(e);
		} catch (UnityException e) {
			throw new LauncherException(e);
		}
	}

	/**
	 * 
	 * @param evt
	 *            non <code>null</code>
	 * @param bus
	 *            use <code>null</code> to get bus automatically
	 * @return
	 */
	private static Response send(QuicklistEvent evt, Bus bus) {
		assert evt != null : "evt is null";
		try {
			if (bus == null)
				bus = Unity.connect();
			return Unity.connect().send(
					Request.newBuilder()
							.setExtension(LauncherPb.quicklistEvent, evt)
							.build());

		} catch (IllegalStateException | IOException e) {
			throw new LauncherException(e);
		} catch (UnityException e) {
			throw new LauncherException(e);
		}
	}

	private static class CreateQuicklistOnInit implements InitHandler {

		private final WeakReference<Quicklist> ref;

		private CreateQuicklistOnInit(Quicklist quicklist) {
			assert quicklist != null;
			this.ref = new WeakReference<>(quicklist);
		}

		@Override
		public void onInit(Bus conn) {
			Quicklist quicklist = ref.get();
			if (quicklist != null)
				create(quicklist, conn);
			else {
				// TODO check if unregistering InitHandler in their own onInit() method
				// does not cause errors
				Unity.unregister(this);
			}
		}

	}

	private static class CreateItemOnInit implements InitHandler {

		private final WeakReference<Item> ref;

		private CreateItemOnInit(Item item) {
			assert item != null;
			this.ref = new WeakReference<>(item);
		}

		@Override
		public void onInit(Bus bus) {
			Item item = ref.get();
			if (item != null) {
				create(item, bus);
				// logger.log(Level.INFO, "item created", item);
			} else {
				// TODO check if unregistering InitHandler in their own onInit() method
				// does not cause errors
				Unity.unregister(this);
			}
		}
	}
}
